﻿using System;
using System.Linq;
using System.Text;
using System.Collections.Generic;
using System.Runtime.InteropServices;

namespace FATLib
{

    struct FatEntry
    {
        private byte[] entry;

        public string filename;
        public string long_filename;
        public FatEntryAttributes attributes;
        private byte case_info;
        public DateTime create_time;
        public DateTime last_access_time;
        public DateTime last_modified_time;
        public long start_cluster;
        public long file_size;


        private bool m_IsDeleted;
        //private bool m_IsLFNEntry;

        private List<FatEntry> m_Entries;
        private List<byte[]> m_LFNEntries;

        private int m_Sortedness;

        private static readonly string[] special_dirs = { ".", ".." };

        /**
         * A FAT entry contains:
         * pos  len data
         * 0x00 11  filename
         * 0x0B 1   attributes
         * 0x0C 1   case_info
         * 0x0D 1   Create time, fine resolution: 10ms units, values from 0 to 199.
         * 0x0E 2   Create time. The hour, minute and second are encoded according to the following bitmap:
         *          Bits    Description
         *          15‑11 	Hours (0-23)
         *          10‑5	    Minutes (0-59)
         *          4‑0	    Seconds/2 (0-29)
         *          Note that the seconds is recorded only to a 2 second resolution. Finer resolution for file creation is found at offset 0x0d.
         * 0x10 2   Create date. The year, month and day are encoded according to the following bitmap:
         *          Bits	Description                     tmp = BitConverter.ToUInt16(buff, 0x10);
         *          15‑9	    Year (0 = 1980, 127 = 2107)     ((tmp >> 9) & 0x7F) + 1980
         *          8‑5	    Month (1 - 12)                  (tmp >> 5) & 0x0F
         *          4‑0	    Day (1 - 31)                    tmp & 0x1F
         * 0x12 2   Last access date; see offset 0x10 for description.
         * 0x14 2   high bytes cluster
         * 0x16 2   Last modified time; see offset 0x0e for description.
         * 0x18 2   Last modified date; see offset 0x10 for description.
         * 0x1A 2   low bytes cluster
         * 0x1C 4   File size in bytes. Entries with the Volume Label or Subdirectory flag set should have a size of 0.
         * */

        public static bool IsValidLFNEntry(byte[] buff)
        {
            return (buff[0x00] & 0x80) == 0 && buff[0x0B] == 0x0F;
        }
        public static bool IsDeletedLFNEntry(byte[] buff)
        {
            return (buff[0x00] & 0x80) == 0x80 && buff[0x0B] == 0x0F;
        }

        public FatEntry(byte fat, byte[] buff, List<byte[]> lfn_buff)
        {

            this.entry = (byte[])buff.Clone();

            this.filename = Encoding.ASCII.GetString( buff, 0, 11 );
            this.long_filename = null;
            this.attributes = (FatEntryAttributes)buff[0x0B];
            this.case_info = buff[0x0C];
            this.create_time = DateTime.MinValue;
            this.last_access_time = DateTime.MinValue;
            this.last_modified_time = DateTime.MinValue;

            this.start_cluster = ( (long)buff[0x1B] << 8 ) + ( (long)buff[0x1A] );
            if( fat == 32 )
            {
                start_cluster += ( (long)buff[0x15] << 24 ) + ( (long)buff[0x14] << 16 );
            }

            this.file_size = BitConverter.ToInt32( buff, 0x1C );

            this.m_IsDeleted = false;

            this.m_Entries = new List<FatEntry>();
            this.m_LFNEntries = new List<byte[]>(lfn_buff);

            this.m_Sortedness = 0;

            if( !IsLFNEntry() )
            {
                // LFN
                this.ParseLFNName();

                // times
                this.create_time = GetDateTime(0x10, 0x0E, buff[0x0D]);
                this.last_modified_time = GetDateTime(0x18, 0x16, 0);
                this.last_access_time = GetDateTime(0x12, -1, 0);

                m_IsDeleted = entry[0] == 0xE5;

                if( filename[0] == 0x05 ) filename = "\xE5" + filename.Substring( 1 );

                if( !IsDeleted() )
                {
                    string basename = filename.Substring( 0, 8 ).TrimEnd();
                    string extension = filename.Substring( 8, 3 ).TrimEnd();

                    if( long_filename == null )
                    {
                        if( ( case_info & 0x08 ) == 0x08 ) { basename = basename.ToLower(); } // basename
                        if( ( case_info & 0x10 ) == 0x10 ) { extension = extension.ToLower(); } // ext
                        this.long_filename = basename;
                        if( extension.Length > 0 ) this.long_filename += "." + extension;
                    }
                }
            }
        }

        public FatEntry(long cluster)
        {
            this.entry = null;
            this.filename = null;
            this.long_filename = null;
            this.attributes = (FatEntryAttributes)0x10; // this constructor is only for the root dir anyways
            this.case_info = 0;
            this.create_time = DateTime.MinValue;
            this.last_access_time = DateTime.MinValue;
            this.last_modified_time = DateTime.MinValue;

            this.file_size = 0;

            this.start_cluster = cluster;

            this.m_IsDeleted = false;

            this.m_Entries = new List<FatEntry>();
            this.m_LFNEntries = new List<byte[]>();

            this.m_Sortedness = 0;
        }

        public bool IsDeleted()
        {
            return this.m_IsDeleted;
        }

        public bool IsLFNEntry()
        {
            return (int)attributes == 0x0F;
        }

        public bool IsVolumeLabel()
        {
            return !IsLFNEntry() && (attributes & FatEntryAttributes.VolumeLabel) != 0;
        }

        public bool IsDirectory()
        {
            return !IsLFNEntry() && !IsVolumeLabel() && ( attributes & FatEntryAttributes.Subdirectory ) != 0;
        }

        public bool IsRegularFile()
        {
            return !IsLFNEntry() && !IsVolumeLabel() && !IsDirectory();
        }

        internal void ParseLFNName()
        {
            if (this.m_LFNEntries != null && this.m_LFNEntries.Count > 0)
            {
                int real_checksum = lfn_checksum();
                int fake_checksum = m_LFNEntries.Last()[0x0D];

                var list = m_LFNEntries.Where(x => x[0x0D] == fake_checksum).ToList();

                if (list.Count == 0) return;

                if (list.Count != m_LFNEntries.Count)
                {
                    m_LFNEntries = new List<byte[]>(list);
                }

                var lfn_buff = new byte[list.Count * 26]; // 26 bytes max in each entry

                list.Reverse();

                for (int i = 0; i < list.Count; i++)
                {
                    Array.Copy(list[i], 0x01, lfn_buff, (i * 26) + 0, 10);
                    Array.Copy(list[i], 0x0E, lfn_buff, (i * 26) + 10, 12);
                    Array.Copy(list[i], 0x1C, lfn_buff, (i * 26) + 22, 4);
                }
                int length = lfn_buff.Length;
                while(length > 0)
                {
                    if ((lfn_buff[length - 2] == 0 && lfn_buff[length - 1] == 0) || (lfn_buff[length - 2] == 255 && lfn_buff[length - 1] == 255)) length -= 2;
                    else break;
                }

                this.long_filename = Encoding.Unicode.GetString(lfn_buff, 0, length);
                int a = 0;
            }
        }

        private byte lfn_checksum()
        {
                int i, sum;
                for (sum = i = 0; i < 11; i++)
                    sum = (((sum & 1) << 7) | ((sum & 0xfe) >> 1)) + entry[i]; 
                return (byte)sum;
        }

        /*public string ConvertToLFN( ref byte lfn_checksum )
        {
            if( !IsLFNEntry() ) return null;

            if( ( entry[0] & 0x80 ) == 0x80 ) return null; // deleted

            if( ( entry[0] & 0x40 ) != 0x40 && lfn_checksum != entry[0x0D] ) return null;
            lfn_checksum = entry[0x0D];

            var tmp = new byte[26];
            Array.Copy( entry, 0x01, tmp, 0, 10 );
            Array.Copy( entry, 0x0E, tmp, 10, 12 );
            Array.Copy( entry, 0x1C, tmp, 22, 4 );

            if( tmp[0] == 255 && tmp[1] == 255 ) return null;

            int length = 26;
            while( length > 0 )
            {
                if( ( tmp[length - 2] == 0 && tmp[length - 1] == 0 ) || ( tmp[length - 2] == 255 && tmp[length - 1] == 255 ) ) length -= 2;
                else break;
            }

            if( length == 0 ) return null;

            var retval = Encoding.Unicode.GetString( tmp, 0, length );
            //Console.WriteLine("{{{0}}}", retval);
            //if( long_filename != null ) retval = retval + long_filename;
            return retval;
        }*/

        public string GetFilename()
        {
            return long_filename == null ? filename : long_filename;
        }

        public void AddSubEntry(FatEntry subentry)
        {
            if (!IsDirectory()) return;
            this.m_Entries.Add(subentry);
        }

        public List<FatEntry> Entries { get { return this.m_Entries; } }

        public override string ToString()
        {
            return GetFilename();
        }

        public void Print()
        {
            Print(0);
        }
        private void Print(int d)
        {
            foreach (var e in m_Entries)
            {
                Console.Write(new String(' ', d));
                Console.Write("{0:yyyy-MM-dd  HH:mm:ss.ff} ", e.last_modified_time);
                Console.Write(e.IsDirectory() ? "<DIR>" : "     ");
                Console.Write(" {0,-40} ", e.GetFilename());
                Console.WriteLine();
                if (e.IsDirectory()) e.Print(d + 1);
            }
        }
        
        public void PrintSortedNess()
        {
            PrintSortedNess(0);
        }
        private void PrintSortedNess(int d)
        {
            var files = m_Entries.Where(x => x.IsRegularFile()).ToList();

            var sorted_files = files.ToList();
            sorted_files.Sort(delegate(FatEntry a, FatEntry b) { return a.GetFilename().CompareTo(b.GetFilename()); });

            var sorting_errors = new List<FatEntry>();
            for (int i = 0; i < files.Count; i++)
            {
                if (!files[i].Equals(sorted_files[i])) sorting_errors.Add(files[i]);
            }
            if (sorting_errors.Count > 0)
            {
                Console.Write(new String(' ', d));
                Console.WriteLine("[{0}] has {1} errors:", GetFilename(), sorting_errors.Count);
                Console.Write(new String(' ', d));
                Console.WriteLine("This directory entry can be found at: {0}", start_cluster);
                for (int i = 0; i < sorted_files.Count; i++)
                {
                    var entry = sorted_files[i];
                    Console.Write(new String(' ', d));
                    Console.Write("- [{0}]", entry.GetFilename());
                    if (!sorted_files[i].Equals(files[i]))
                    {
                        Console.Write(" <-- needs to be sorted");
                    }
                    Console.WriteLine();
                }
            }
        }

        public bool IsSortedCorrectly()
        {
            var files = m_Entries.Where(x => x.IsRegularFile()).ToList();

            var sorted_files = files.ToList();
            sorted_files.Sort(delegate(FatEntry a, FatEntry b) { return a.GetFilename().CompareTo(b.GetFilename()); });

            for (int i = 0; i < files.Count; i++)
            {
                if (!files[i].Equals(sorted_files[i])) return false;
            }
            return true;
        }

        public List<FatEntry> GetSortedList()
        {
            var files = m_Entries.Where(x => x.IsRegularFile()).ToList();
            files.Sort(delegate(FatEntry a, FatEntry b) { return a.GetFilename().CompareTo(b.GetFilename()); });
            return m_Entries.Where(x => !x.IsRegularFile()).Union(files).ToList();
        }

        public byte[] GetRawBuffer(bool include_lfn_entries)
        {
            if (!include_lfn_entries) return this.entry;

            var list = new List<byte>();
            foreach (var x in m_LFNEntries) list.AddRange(x);
            list.AddRange(this.entry);
            return list.ToArray();
        }

        private DateTime GetDateTime(int date_offset, int time_offset, byte time_fine)
        {
            UInt16 raw_date = BitConverter.ToUInt16(entry, date_offset);
            UInt16 raw_time = 0;
            if (time_offset >= 0) raw_time = BitConverter.ToUInt16(entry, time_offset);
            if (raw_date > 0)
            {
                int seconds = (raw_time & 0x1F) * 2;
                int millisecs = time_fine * 10;
                while(millisecs > 999)
                {
                    millisecs -= 1000;
                    seconds += 1;
                }
                try
                {

                    return new DateTime(
                        ((raw_date >> 9) & 0x7F) + 1980, (raw_date >> 5) & 0x0F, raw_date & 0x1F,
                        (raw_time >> 11) & 0x1F, (raw_time >> 5) & 0x3F, seconds, millisecs
                        );
                }
                catch (ArgumentOutOfRangeException)
                { }
            }
            return DateTime.MinValue;
        }
    }

    [Flags]
    public enum FatEntryAttributes
    {
        ReadOnly = 0x01,
        Hidden = 0x02,
        System = 0x04,
        VolumeLabel = 0x08,
        Subdirectory = 0x10,
        Archive = 0x20,
        Device = 0x40,
        Unused = 0x80
    }

    #region troep

    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    struct FatEntryRaw
    {
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
        byte[] filename;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
        byte[] extension;
        byte attributes;
        byte case_info;
        byte create_time_fine;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
        byte[] create_time;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
        byte[] create_date;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
        byte[] last_access_date;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
        byte[] cluster_high_bytes;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
        byte[] last_modified_time;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
        byte[] last_modified_date;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
        byte[] cluster_low_bytes;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
        byte[] file_size;

        public static FatEntryRaw GetValue(byte[] buff)
        {
            GCHandle handle = GCHandle.Alloc(buff, GCHandleType.Pinned);
            FatEntryRaw structure = (FatEntryRaw)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(FatEntryRaw));
            handle.Free();
            return structure;
        }
    }
#endregion
}